package ontime

import (
	"context"
	"math"
	"sort"
	"sync"
	"time"
)

type OnTime struct {
	entries   []*Entry
	stop      chan struct{}
	add       chan *Entry
	running   bool
	runningMu sync.Mutex
	// when true,we can save the uncompleted tasks and
	// abandon,i do not know how to serial a function
	//save      bool // abandon
	//store     Store // abandon
	logger    Logger
	beforeRun func()
	afterRun  func([]*Entry)
	WaitGroup sync.WaitGroup
	nextId    int64
	remove    chan int64
}

func New(options ...Option) *OnTime {
	onTime := &OnTime{
		entries:   nil,
		stop:      make(chan struct{}),
		add:       make(chan *Entry),
		remove:    make(chan int64),
		running:   false,
		runningMu: sync.Mutex{},
		logger:    DefaultLogger,
	}
	for _, option := range options {
		option(onTime)
	}
	//if onTime.save && onTime.store != nil {
	//	entries, err := onTime.store.Read()
	//	if err != nil {
	//		onTime.entries = entries
	//	}
	//}
	return onTime
}

func (onTime *OnTime) Start() {
	onTime.runningMu.Lock()
	defer onTime.runningMu.Unlock()
	if onTime.running {
		return
	}
	onTime.running = true
	onTime.checkInvalidEntries()
	onTime.logger.Info("Starting...")
	go onTime.run()
}

func (onTime *OnTime) checkInvalidEntries() {
	es := make([]*Entry, 0)
	now := time.Now()
	for _, v := range onTime.entries {
		if now.Sub(v.ExpireAt) <= 0 {
			es = append(es, v)
		}
	}
	onTime.entries = es
}

func (onTime *OnTime) run() {
	now := time.Now()
	for {
		sort.Sort(EntryChain(onTime.entries))
		var timer *time.Timer
		if len(onTime.entries) == 0 {
			timer = time.NewTimer(time.Hour * 10000)
		} else {
			timer = time.NewTimer(onTime.entries[0].ExpireAt.Sub(now))
		}
		for {
			select {
			case now = <-timer.C:
				newEntries := make([]*Entry, 0)
				for _, entry := range onTime.entries {
					if entry.ExpireAt.Sub(now) <= 0 {
						onTime.WaitGroup.Add(1)
						go func(entry *Entry) {
							defer onTime.WaitGroup.Done()
							if onTime.beforeRun != nil {
								onTime.beforeRun()
							}
							if entry.Job != nil {
								err := entry.Job()
								if err != nil {
									onTime.logger.Error("execute error:", err.Error())
								}
							}
						}(entry)
					} else {
						newEntries = append(newEntries, entry)
					}
				}
				onTime.entries = newEntries
			case newEntry := <-onTime.add:
				timer.Stop()
				now = time.Now()
				onTime.entries = append(onTime.entries, newEntry)
			case id := <-onTime.remove:
				timer.Stop()
				now = time.Now()
				onTime.removeEntry(id)
			case <-onTime.stop:
				timer.Stop()
				onTime.logger.Info("Stopping...")
				return
			}
			break

		}
	}
}

// Add add one job function with expire
func (onTime *OnTime) Add(job Job, expireAt time.Time) int64 {
	if time.Now().After(expireAt) {
		return -1
	}
	onTime.runningMu.Lock()
	defer onTime.runningMu.Unlock()
	onTime.nextId++
	// although it cannot reach this....
	if onTime.nextId == math.MaxInt64 {
		onTime.nextId = 1
	}
	entry := &Entry{
		Job:      job,
		ExpireAt: expireAt,
		EntryId:  onTime.nextId,
	}
	if onTime.running {
		onTime.add <- entry
	} else {
		onTime.entries = append(onTime.entries, entry)
	}
	return entry.EntryId
}

func (onTime *OnTime) Stop() context.Context {
	if !onTime.running {
		return context.Background()
	}
	onTime.running = false
	onTime.stop <- struct{}{}
	// waiting all tasks are completed
	ctx, cancel := context.WithCancel(context.Background())
	go func() {
		onTime.WaitGroup.Wait()
		cancel()
	}()
	onTime.WaitGroup.Wait()
	if onTime.afterRun != nil {
		onTime.afterRun(onTime.entries)
	}
	onTime.logger.Info("Stopped")
	return ctx
}

func (onTime *OnTime) Remove(entryId int64) {
	onTime.runningMu.Lock()
	defer onTime.runningMu.Unlock()
	if onTime.running {
		onTime.remove <- entryId
	} else {
		onTime.removeEntry(entryId)
	}
}

func (onTime *OnTime) removeEntry(entryId int64) {
	onTime.runningMu.Lock()
	defer onTime.runningMu.Unlock()
	var dest int = -1
	for k, e := range onTime.entries {
		if e.EntryId == entryId {
			dest = k
			break
		}
	}
	// found
	if dest > -1 {
		onTime.entries = append(onTime.entries[:dest], onTime.entries[dest+1:]...)
	}
}

func (onTime *OnTime) removeEntryByUniqueTag(uniqueTag string) {
	onTime.runningMu.Lock()
	defer onTime.runningMu.Unlock()
	var dest int = -1
	for k, e := range onTime.entries {
		if e.UniqueTag == uniqueTag {
			dest = k
			break
		}
	}
	// found
	if dest > -1 {
		onTime.entries = append(onTime.entries[:dest], onTime.entries[dest+1:]...)
	}
}
